#!/usr/bin/ruby
#Copyright (c) 2011 Mark Watts
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
require 'fusefs'
require 'xmmsclient'
include FuseFS

def sanitise(str)
    str.gsub!(%r{/}, "::-")
    str.gsub(%r{\\}, "%5C")
end
def desanitise(str)
    str.gsub!(%r{::-}, '/')
    str.gsub!(%r{%5C}, '\\')
    str.gsub!(%r{'}, "\'")
    str.gsub(%r{"}, "\"")
end
class XmmsFS < FuseFS::FuseDir
    def initialize
        @xmms = Xmms::Client.new("XMMSFS")
        @xmms.connect(ENV["XMMS_PATH"])
  
        @xmms.on_disconnect do
            puts "daemon exited, shutting down"
            FuseFS.exit
        end
        @universe = Xmms::Collection.universe
        @collection = @universe
        @prop_cache = Array.new # saves from having to requery each time we look at a media property
    end
    # runs for any given path 
    # basically, @collection is the "current" collection based off of the
    # path. contents() gives out a list of files which is either
    # 1. possible values for a field
    # 2. ids in @collection
    # 3. regular files which represent properties for a given id number
    # reads all of the ids from it
    # we use base as in basename, so it's really the current directory
    def contents(path)
        items = scan_path(path)
        base = nil
        if !items.empty?
        # What to do if we're at the root of our mount
            if (items.size % 2 != 0)
            # A bit of a hack. A collections query has field:value pairs joined by
            # something like "AND" or "OR" if the number of directories is odd, that
            # means we only have half a pair, and we can't query with field:
                base = items.pop
            end
            if !items.empty?
            # generates the query string for a new collection
                parse_string = ""
                # basically, every couple of directories constitutes a query
                # like "artist/Prince" in the path corresponds to "artist:Prince"
                # in parse_string
                items.each_slice(2) do |slice|
                    # numerical comparators for stuff like bitrates can be in paths
                    if !(sep = slice[1].slice!(/^(<=)|^(>=)|^<|^>/)).nil?
                        # not sure why, but I don't want to compare to 0
                        if (slice[1].to_i != 0)
                            parse_string << "#{slice[0]}#{sep}#{slice[1]} "
                        end
                    else
                        # i don't really like that queries like this are almost directly
                        # from the pathname, but oh well.
                        parse_string << "#{slice[0]}:\"#{desanitise(slice[1])}\" "
                    end
                end
                @collection = Xmms::Collection.parse(parse_string)
            else
            # Means we have only one directory off of the mountpoint
                @collection = @universe
            end
            if !base.nil?
            # Means we only have the field part of a field:value part to a query
            # in that case we print directories of possible values for that field
            # instead of id numbers in the containing collection
                if (base == 'in')
                    return @xmms.coll_list(Xmms::Collection::NS_COLLECTIONS).wait.value.collect{ |name| 
                    "Collections::-" + name} + @xmms.coll_list(Xmms::Collection::NS_PLAYLISTS).wait.value.collect { |name| 
                    "Playlists::-" + name }
                end
                if !(base =~ /\d+/).nil?
                # Print out media properties for an id of base
                # Another *slightly* kludgy solution. Since no field names are simple numbers
                # this should always work.
                    files = []
                    base = base.to_i
                    # @prop_cache[0] contains a media id and @prop_cache[1] is the info
                    if base != @prop_cache[0]
                        @prop_cache = [base, @xmms.medialib_get_info(base).wait.value]
                    end
                    @prop_cache[1].each do |value, dict|
                        files << sanitise(value.to_s)
                    end
                    return files
                end
                dict = @xmms.coll_query_info(@collection, base).wait.value
                if !dict.nil?
                    files = []
                    dict.collect{|b| b.collect{|t,j| j}}.flatten.compact.sort.each do |i|
                        files << sanitise(i.to_s)
                    end
                    return files
                end
            end
        else
            @collection = @universe
        end
        if (!(a = @xmms.coll_query_ids(@collection).wait.value).empty?)
            files = []
            a.each do |id| 
                files << id.to_s
            end
        end
        files
    end
    # FuseDir virtual methods, must override
    def directory?(path)
        !file?(path)
    end
    def can_mkdir?(path)
        true
    end
    def can_rmdir?(path)
        true
    end
    def file?(path)
        items = scan_path(path)
        if (items.size % 2 != 0)
            return false
        end
        id, property = items.pop(2)
        id = id.to_i
        # id 0 is like an error id - media id numbering
        # starts with 1
        if id == 0
            return false
        end
        # considering caching more than 1 id. don't know if worth while
        if @prop_cache[0] != id
            @prop_cache = [id, @xmms.medialib_get_info(id).wait.value]
        end
        if !@prop_cache[1].nil? and @prop_cache[1].has_key?(:"#{desanitise(property)}")
            return true
        else
            return false
        end
    end
    # only called for regular files so we don't have to check
    def read_file(path)
        items = scan_path(path)
        property = items.pop
        # @prop_cache is guaranteed to only contain info for the "current" media id
        @prop_cache[1][:"#{desanitise(property)}"].first.last.to_s
    end
    def can_write?(path)
        true
    end
    # plan on _maybe_ doing some special stuff here,
    # maybe...
    def write_to(path, str)
        true
    end
end
if (File.basename($0) == File.basename(__FILE__))
# no clue why I do this check. like it would ever prove false?
  if (ARGV.size != 1)
    puts "Usage: #{$0} <directory>"
    exit
  end

  dirname = ARGV.shift

  unless File.directory?(dirname)
    puts "Error: #{dirname} is not a directory."
    exit
  end
  root = XmmsFS.new
  FuseFS.set_root(root)

  FuseFS.mount_under(dirname)

  FuseFS.run # This doesn't return until we're unmounted.
  FuseFS.unmount # This doesn't actually unmount, but we need to keep it for some reason...
end
